export type EventType = string | symbol

// An event handler can take an optional event argument
// and should not return a value
export type Handler<T = any> = (event?: T) => void
export type WildcardHandler = (event: any, type?: EventType) => void

// An array of all currently registered event handlers for a type
export type EventHandlerList = Array<Handler>
export type WildCardEventHandlerList = Array<WildcardHandler>

// A map of event types and their corresponding event handlers.
export type EventHandlerMap = Map<
  EventType,
  EventHandlerList | WildCardEventHandlerList
>

export interface Emitter {
  // all: EventHandlerMap

  on<T = any>(type: EventType, handler: Handler<T>): void
  on(type: '*', handler: WildcardHandler): void

  off<T = any>(type: EventType, handler: Handler<T>): void
  off(type: '*', handler: WildcardHandler): void

  emit<T = any>(type: EventType, event?: T): void
  emit(type: '*', event?: any): void
}

class Base implements Emitter {
  private readonly all: EventHandlerMap = new Map()
  on(type: EventType, handler: WildcardHandler): void {
    const handlers = this.all.get(type)
    const added = handlers && handlers.push(handler)
    if (!added) {
      this.all.set(type, [handler])
    }
  }

  once(type: EventType, handler: WildcardHandler): void {
    const wrappedHandler: WildcardHandler = (event: any) => {
      this.off(type, wrappedHandler)
      handler(event, type)
    }
    const handlers = this.all.get(type)
    const added = handlers && handlers.push(wrappedHandler)
    if (!added) {
      this.all.set(type, [wrappedHandler])
    }
  }

  off(type: EventType, handler: WildcardHandler): void {
    const handlers = this.all.get(type)
    if (handlers) {
      handlers.splice(handlers.indexOf(handler) >>> 0, 1)
    }
  }

  emit(type: EventType, event?: any): void {
    ;((this.all.get(type) || []) as WildCardEventHandlerList)
      .slice()
      .forEach((handler) => {
        handler(event, type)
      })
    ;((this.all.get('*') || []) as WildCardEventHandlerList)
      .slice()
      .forEach((handler) => {
        handler(event, type)
      })
  }
}

export interface styleOptions {
  stroke?: string
  strokeWidth?: number
  fill?: string
}

export interface PainterOptions {
  container?: HTMLElement
  type?: string
}

export interface PathOptions {
  start: Point
  end?: Point
  points?: Array<Point>
}

interface Point {
  x: number
  y: number
}

interface Control {
  type: string
  el: SVGElement
  value?: any
  painting: (point: Point) => void
  draw: (stageRect: DOMRect) => void
}

class Path implements Control {
  type = 'path'
  private stroke = '#000'
  private strokeWidth = 2
  private fill = 'none'
  private start: Point
  private points: Array<Point>
  el: SVGPathElement
  constructor(options: PathOptions & styleOptions) {
    this.start = options.start
    this.stroke = options.stroke || this.stroke
    this.strokeWidth = options.strokeWidth || this.strokeWidth
    this.fill = options.fill || this.fill
    this.points = options.points || []
    this.el = document.createElementNS('http://www.w3.org/2000/svg', 'path')
    this.el.setAttribute('fill', this.fill)
    this.el.setAttribute('stroke', this.stroke)
    this.el.setAttribute('stroke-width', this.strokeWidth.toString())
  }
  painting(point: Point) {
    this.points.push(point)
  }
  draw(stageRect: DOMRect) {
    let dArr = [
      `M${this.start.x * stageRect.width} ${this.start.y * stageRect.height}`,
      ...this.points.map(
        (p) => `L${p.x * stageRect.width} ${p.y * stageRect.height}`
      ),
    ]
    this.el.setAttribute('d', dArr.join(' '))
  }
}

class Arrow implements Control {
  type = 'arrow'
  private stroke = '#000'
  private strokeWidth = 2
  private fill = 'none'
  private start: Point
  private end: Point
  el: SVGPathElement
  constructor(options: PathOptions & styleOptions) {
    this.start = options.start
    this.end = options.end || options.start
    this.stroke = options.stroke || this.stroke
    this.strokeWidth = options.strokeWidth || this.strokeWidth
    this.fill = options.fill || this.fill
    this.el = document.createElementNS('http://www.w3.org/2000/svg', 'path')
    this.el.setAttribute('fill', this.fill)
    this.el.setAttribute('stroke', this.stroke)
    this.el.setAttribute('stroke-width', this.strokeWidth.toString())
  }
  private getArrowD(stageRect: DOMRect): string {
    var t = this.start.x * stageRect.width,
      a = this.start.y * stageRect.height,
      n = this.end.x * stageRect.width,
      r = this.end.y * stageRect.height,
      i = function (e: number, t: number, a: number, n: number, r: number) {
        return {
          x: t + e * (n - t),
          y: a + e * (r - a),
        }
      },
      o = function (e: number, t: number, a: number, n: number, r: number) {
        return {
          x: (n - t) * Math.cos(e) - (r - a) * Math.sin(e) + t,
          y: (n - t) * Math.sin(e) + (r - a) * Math.cos(e) + a,
        }
      },
      c = i(0.8, t, a, n, r),
      s = c.x,
      l = c.y,
      u = o(Math.PI / 2, s, l, n, r),
      d = u.x,
      p = u.y,
      m = o((Math.PI / 2) * 3, s, l, n, r),
      f = m.x,
      h = m.y,
      b = i(0.4, d, p, f, h),
      g = b.x,
      v = b.y,
      y = i(0.6, d, p, f, h),
      O = y.x,
      E = y.y,
      j = i(0.3, d, p, f, h),
      x = j.x,
      k = j.y,
      w = i(0.7, d, p, f, h),
      C = w.x,
      _ = w.y
    return 'M'
      .concat(t.toString(), ' ')
      .concat(a.toString(), ' L')
      .concat(g.toString(), ' ')
      .concat(v.toString(), ' L')
      .concat(x.toString(), ' ')
      .concat(k.toString(), ' L')
      .concat(n.toString(), ' ')
      .concat(r.toString(), ' L')
      .concat(C.toString(), ' ')
      .concat(_.toString(), ' L')
      .concat(O.toString(), ' ')
      .concat(E.toString(), ' Z')
  }
  painting(point: Point) {
    this.end = point
  }
  draw(stageRect: DOMRect) {
    let d = this.getArrowD(stageRect)
    this.el.setAttribute('d', d)
  }
}

class Rect implements Control {
  private stroke = '#000'
  type = 'rect'
  private strokeWidth = 2
  private fill = 'none'
  private start: Point
  private end: Point
  el: SVGRectElement
  constructor(options: PathOptions & styleOptions) {
    this.start = options.start
    this.end = options.end || options.start
    this.stroke = options.stroke || this.stroke
    this.strokeWidth = options.strokeWidth || this.strokeWidth
    this.fill = options.fill || this.fill
    this.el = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
    this.el.setAttribute('fill', this.fill)
    this.el.setAttribute('stroke', this.stroke)
    this.el.setAttribute('stroke-width', this.strokeWidth.toString())
  }
  painting(point: Point) {
    this.end = point
  }
  draw(stageRect: DOMRect) {
    this.el.setAttribute(
      'x',
      (stageRect.width * Math.min(this.start.x, this.end.x)).toString()
    )
    this.el.setAttribute(
      'y',
      (stageRect.height * Math.min(this.start.y, this.end.y)).toString()
    )
    this.el.setAttribute(
      'width',
      (stageRect.width * Math.abs(this.start.x - this.end.x)).toString()
    )
    this.el.setAttribute(
      'height',
      (stageRect.height * Math.abs(this.start.y - this.end.y)).toString()
    )
  }
}

class Text implements Control {
  private stroke = '#000'
  type = 'text'
  private fill = '#fff'
  value = ''
  private start: Point
  el: SVGTextElement
  constructor(options: PathOptions & styleOptions) {
    this.start = options.start
    this.stroke = options.stroke || this.stroke
    this.fill = options.fill || this.fill
    this.el = document.createElementNS('http://www.w3.org/2000/svg', 'text')
    this.el.setAttribute('font-size', '20px')
    this.el.setAttribute('font-family', '微软雅黑')
    this.el.setAttribute('fill', this.fill)
  }
  painting(point: Point) {
    this.start = point
  }
  draw(stageRect: DOMRect) {
    this.el.innerHTML = ''
    let baseX = stageRect.width * this.start.x + 1
    const baseY = stageRect.height * this.start.y + 23
    this.el.setAttribute('x', baseX.toString())
    this.el.setAttribute('y', baseY.toString())
    const valueArr = this.value.split('\n')
    const tspanArr: Array<SVGTSpanElement> = []
    let lineNumber = 0
    valueArr.forEach((value) => {
      const wordArr = value.split('')
      let word
      let line = ''
      let tspan = document.createElementNS(
        'http://www.w3.org/2000/svg',
        'tspan'
      )
      tspan.setAttribute('x', baseX.toString())
      tspan.setAttribute('y', (baseY + lineNumber * 28).toString())
      lineNumber++
      this.el.appendChild(tspan)
      while ((word = wordArr.shift())) {
        line += word
        tspan.textContent = line
        if (tspan.getComputedTextLength() > 200) {
          tspan.textContent = line.slice(0, line.length - 1)
          tspanArr.push(tspan)
          line = word
          tspan = document.createElementNS(
            'http://www.w3.org/2000/svg',
            'tspan'
          )
          tspan.setAttribute('x', baseX.toString())
          tspan.setAttribute('y', (baseY + lineNumber * 28).toString())
          this.el.appendChild(tspan)
          lineNumber++
        }
      }
    })
  }
}

class TextEditor extends Base {
  el: HTMLDivElement
  callback = (val: string) => {}
  input: HTMLDivElement
  constructor(options: styleOptions) {
    super()
    this.el = document.createElement('div')
    this.el.setAttribute(
      'style',
      'height: 100%; width: 100%; position: absolute;top:0;display:none;'
    )
    this.input = document.createElement('div')
    this.input.setAttribute('contenteditable', 'true')
    this.input.setAttribute(
      'style',
      `border: .0625rem solid red; outline: none;min-width: .625rem; word-wrap: break-word;word-break: break-all;-webkit-user-select: auto;z-index: 100;position: absolute;background: none;font-size: 20px;font-family: 微软雅黑;color: ${
        options.stroke || 'rgb(241, 62, 73)'
      };width: 200px;line-height:28px;`
    )
    this.el.appendChild(this.input)
    this.input.onmousedown = (e) => e.stopPropagation()
    this.input.onmousemove = (e) => e.stopPropagation()
    this.input.onmouseup = (e) => e.stopPropagation()

    this.input.onblur = (e) => this.onBlur(e)
    this.input.onkeyup = (e) => this.onKeyup(e)
  }
  ative(e: MouseEvent, callback: (val: string) => void) {
    if (this.input.innerText) {
      this.callback(this.input.innerText)
    }
    this.callback = callback
    this.input.style.left = `${e.offsetX}px`
    this.input.style.top = `${e.offsetY}px`
    this.el.style.display = 'block'
    this.input.focus()
    this.input.innerText = ''
  }
  setStyle(options: styleOptions) {
    this.input.style.color = options.stroke || this.input.style.color
    this.input.style.border = options.stroke
      ? `.0625rem solid ${options.stroke}`
      : this.input.style.color
  }
  onBlur(e: FocusEvent) {
    this.el.style.display = 'none'
    this.callback(this.input.innerText)
  }
  onKeyup(e: KeyboardEvent) {
    this.emit('keyup', e)
  }
}

export class Painter extends Base {
  private _el: SVGSVGElement
  private _textEditor: TextEditor
  private stroke = '#000'
  private strokeWidth = 2
  private fill = 'none'
  private type = 'path'
  private currentControl: Control | null
  private controls: Array<Control>
  private history: Array<Control>
  constructor(options: PainterOptions & styleOptions) {
    super()
    this._el = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
    this._el.addEventListener('mousedown', this.startPainting.bind(this))
    this._textEditor = new TextEditor(options)
    this._textEditor.el.addEventListener(
      'mousedown',
      this.startPainting.bind(this)
    )
    this.currentControl = null
    this.controls = []
    this.history = []
    this.setStyle(options)
    this.setPainter(options)
    if (options.container) {
      this.mount(options.container)
    }
  }
  mount(container: HTMLElement) {
    const rect = container.getBoundingClientRect()
    this._el.setAttribute('width', rect.width.toString())
    this._el.setAttribute('height', rect.height.toString())
    container.appendChild(this._el)
    container.appendChild(this._textEditor.el)
    const resizeObserver = new ResizeObserver((entries) => {
      const rect = container.getBoundingClientRect()
      this._el.setAttribute('width', rect.width.toString())
      this._el.setAttribute('height', rect.height.toString())
      for (let entry of entries) {
        this.controls.forEach((control) => {
          control.draw(entry.contentRect)
        })
        this.history.forEach((control) => {
          control.draw(entry.contentRect)
        })
      }
    })
    resizeObserver.observe(container)
    this.painting = this.painting.bind(this)
    this.stopPainting = this.stopPainting.bind(this)
  }
  destory() {
    this._el.parentElement?.removeChild(this._el)
    this._el.parentElement?.removeChild(this._textEditor.el)
  }
  startPainting(e: MouseEvent) {
    document.addEventListener('mousemove', this.painting)
    document.addEventListener('mouseup', this.stopPainting)
    let rect = this._el.getBoundingClientRect()
    let startPoint = {
      x: (e.x - rect.x) / rect.width,
      y: (e.y - rect.y) / rect.height,
    }
    let options = {
      stroke: this.stroke,
      strokeWidth: this.strokeWidth,
      fill: this.fill,
      start: startPoint,
    }
    if (this.type === 'rect') {
      this.currentControl = new Rect(options)
    } else if (this.type === 'arrow') {
      options.fill = this.stroke
      this.currentControl = new Arrow(options)
    } else if (this.type === 'text') {
      options.fill = this.stroke
      this.currentControl = new Text(options)
    } else {
      this.currentControl = new Path(options)
    }

    if (this.currentControl.type !== 'text') {
      this.controls.push(this.currentControl!)
      this._el.appendChild(this.currentControl!.el)
    }

    this.history = []
    this.emit('paintStart', this.currentControl)
  }
  realPaintinge(e: MouseEvent) {
    let rect = this._el.getBoundingClientRect()
    let point = {
      x: (e.x - rect.x) / rect.width,
      y: (e.y - rect.y) / rect.height,
    }
    this.currentControl?.painting(point)
    this.currentControl?.draw(rect)
    this.emit('painting', this.currentControl)
  }

  painting(e: MouseEvent) {
    requestAnimationFrame(() => this.realPaintinge(e))
  }
  stopPainting(e: MouseEvent) {
    this.emit('painted', this.currentControl)

    if (this.currentControl!.type === 'text') {
      const control = this.currentControl
      this._textEditor.ative(e, (val: string) => {
        if (val) {
          control!.value = val
          let rect = this._el.getBoundingClientRect()
          control!.draw(rect)
          this.controls.push(control!)
          this._el.appendChild(control!.el)
        }
      }) // 还未结束
    } else {
      this.currentControl = null
    }
    document.removeEventListener('mousemove', this.painting)
    document.removeEventListener('mouseup', this.stopPainting)
  }
  clear() {
    this.emit('clear', this.controls)
    this.controls = []
    this._el.innerHTML = ''
  }
  undo() {
    if (this.controls.length) {
      let p = this.controls.pop()
      this.history.push(p!)
      this._el.removeChild(p!.el)
      this.emit('undo', p)
    }
  }
  redo() {
    if (this.history.length) {
      let p = this.history.pop()
      this.controls.push(p!)
      this._el.appendChild(p!.el)
      this.emit('redo', p)
    }
  }
  draw() {
    this.controls.forEach((i) => i.draw(this._el.getBoundingClientRect()))
  }
  setStyle(options: styleOptions) {
    this.stroke = options?.stroke || this.stroke
    this.strokeWidth = options?.strokeWidth || this.strokeWidth
    this.fill = options?.fill || this.fill
    this._textEditor.setStyle(options)
  }
  setPainter(options: PainterOptions) {
    this.type = options.type || this.type
  }
  toSvg(): string {
    let serializer = new XMLSerializer()
    const svg = `<?xml version="1.0" standalone="no"?>
${serializer.serializeToString(this._el)}`
    return svg
  }
  toImage(type: string): Promise<string> {
    return new Promise((resolve, reject) => {
      let image = new Image()
      image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(
        this.toSvg()
      )}`
      let canvas = document.createElement('canvas')
      let rect = this._el.getBoundingClientRect()
      canvas.width = rect.width
      canvas.height = rect.height
      let context = canvas.getContext('2d')
      image.onload = function () {
        context!.drawImage(image, 0, 0)
        resolve(canvas.toDataURL(`image/${type}`))
      }
      image.onerror = reject
    })
  }
  composite(type: string, video: HTMLVideoElement): Promise<string> {
    return new Promise((resolve, reject) => {
      let image = new Image()
      image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(
        this.toSvg()
      )}`
      let canvas = document.createElement('canvas')
      let rect = this._el.getBoundingClientRect()
      canvas.width = rect.width
      canvas.height = rect.height
      let context = canvas.getContext('2d')
      image.onload = function () {
        context!.drawImage(
          video,
          0,
          0,
          video.videoWidth,
          video.videoHeight,
          0,
          0,
          canvas.width,
          canvas.height
        )
        context!.drawImage(
          image,
          0,
          0,
          image.width,
          image.height,
          0,
          0,
          canvas.width,
          canvas.height
        )
        resolve(canvas.toDataURL(`image/${type}`, 0.1))
      }
      image.onerror = reject
    })
  }
}
